package server

import (
	"context"
	"dingusxp/trial/guestbook/server/log"
	"dingusxp/trial/guestbook/server/middleware"
	"dingusxp/trial/guestbook/store"
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
)

type GuestbookServer struct {
	store  store.Store
	server *http.Server
}

func NewGuestbookServer(addr string, store store.Store) (*GuestbookServer, error) {

	gs := &GuestbookServer{
		store: store,
		server: &http.Server{
			Addr: addr,
		},
	}

	router := mux.NewRouter()
	router.HandleFunc("/", gs.handleRoot).Methods("GET")
	router.HandleFunc("/note", gs.handleCreate).Methods("POST")
	router.HandleFunc("/note/{id}", gs.handleGet).Methods("GET")
	router.HandleFunc("/note/{id}", gs.handleUpdate).Methods("POST")
	router.HandleFunc("/note/{id}", gs.handleDelete).Methods("DELETE")
	router.HandleFunc("/notes", gs.handleList).Methods("GET")

	gs.server.Handler = middleware.Validating(middleware.Logging(router))
	return gs, nil
}

// server func
func (gs *GuestbookServer) ListenAndServe() (<-chan error, error) {

	var err error
	errChan := make(chan error)
	go func() {
		err = gs.server.ListenAndServe()
		errChan <- err
	}()

	select {
	case err = <-errChan:
		return nil, err
	case <-time.After(time.Second):
		return errChan, nil
	}
}

func (gs *GuestbookServer) Shutdown(ctx context.Context) error {

	return gs.server.Shutdown(ctx)
}

// api response
const (
	CODE_SUCCESS      = uint32(0)
	CODE_SYSTEM_ERROR = uint32(1)
	CODE_PARAM_ERROR  = uint32(2)
)

type ResponseJson struct {
	Code    uint32 `json:"code"`
	Message string `json:"message"`
	Data    string `json:"data"`
}

func responseError(rw http.ResponseWriter, code uint32, message string) {

	response(rw, code, message, nil)
}

func responseSuccess(rw http.ResponseWriter, v interface{}) {

	response(rw, CODE_SUCCESS, "OK", v)
}

func response(rw http.ResponseWriter, code uint32, message string, data interface{}) {

	dataJsonstr, err := json.Marshal(data)
	if err != nil {
		logMsg := "json encode data failed: " + err.Error()
		http.Error(rw, logMsg, http.StatusInternalServerError)
		log.Error(logMsg, "method: response")
		return
	}
	resp := ResponseJson{
		Code:    code,
		Message: message,
		Data:    string(dataJsonstr),
	}
	content, err := json.Marshal(resp)
	if err != nil {
		logMsg := "json encode response failed: " + err.Error()
		http.Error(rw, logMsg, http.StatusInternalServerError)
		log.Error(logMsg, "method: response")
		return
	}
	rw.Header().Set("Content-Type", "application/json")
	rw.Write(content)
}

// router handler
func (gs *GuestbookServer) handleRoot(rw http.ResponseWriter, r *http.Request) {

	responseSuccess(rw, "welcome to guestbook server")
}

func (gs *GuestbookServer) handleCreate(rw http.ResponseWriter, r *http.Request) {

	decoder := json.NewDecoder(r.Body)
	var note store.Note
	if err := decoder.Decode(&note); err != nil {
		logMsg := "parse request body as note failed: " + err.Error()
		log.Warning(logMsg, "method: handleCreate")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	if err := gs.store.Create(&note); err != nil {
		logMsg := "store create failed: " + err.Error()
		log.Error(logMsg, "method: handleCreate")
		responseError(rw, CODE_SYSTEM_ERROR, logMsg)
		return
	}

	responseSuccess(rw, note.Id)
}

func (gs *GuestbookServer) handleUpdate(rw http.ResponseWriter, r *http.Request) {

	idStr, ok := mux.Vars(r)["id"]
	if !ok {
		logMsg := "id not given"
		log.Warning(logMsg, "method: handleUpdate")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		logMsg := "invalid id(convert failed): " + err.Error()
		log.Warning(logMsg, "method: handleUpdate")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	decoder := json.NewDecoder(r.Body)
	var note store.Note
	if err := decoder.Decode(&note); err != nil {
		logMsg := "parse request body as note failed: " + err.Error()
		log.Warning(logMsg, "method: handleUpdate")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	if id != note.Id {
		logMsg := "path id does not match body data(note.Id)"
		log.Warning(logMsg, "method: handleUpdate")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	if err := gs.store.Update(&note); err != nil {
		logMsg := "store update failed: " + err.Error()
		log.Error(logMsg, "method: handleUpdate")
		responseError(rw, CODE_SYSTEM_ERROR, logMsg)
		return
	}

	responseSuccess(rw, true)
}

func (gs *GuestbookServer) handleGet(rw http.ResponseWriter, r *http.Request) {

	idStr, ok := mux.Vars(r)["id"]
	if !ok {
		logMsg := "id not given"
		log.Warning(logMsg, "method: handleGet")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		logMsg := "invalid id(convert failed): " + err.Error()
		log.Warning(logMsg, "method: handleGet")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	note, err := gs.store.Get(id)
	if err != nil {
		logMsg := "store get failed: " + err.Error()
		log.Error(logMsg, "method: handleGet")
		responseError(rw, CODE_SYSTEM_ERROR, logMsg)
		return
	}

	responseSuccess(rw, note)
}

func (gs *GuestbookServer) handleDelete(rw http.ResponseWriter, r *http.Request) {

	idStr, ok := mux.Vars(r)["id"]
	if !ok {
		logMsg := "id not given"
		log.Warning(logMsg, "method: handleDelete")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		logMsg := "invalid id(convert failed): " + err.Error()
		log.Warning(logMsg, "method: handleDelete")
		responseError(rw, CODE_PARAM_ERROR, logMsg)
		return
	}

	err = gs.store.Delete(id)
	if err != nil {
		logMsg := "store delete failed: " + err.Error()
		log.Error(logMsg, "method: handleDelete")
		responseError(rw, CODE_SYSTEM_ERROR, logMsg)
		return
	}

	responseSuccess(rw, true)
}

func (gs *GuestbookServer) handleList(rw http.ResponseWriter, r *http.Request) {

	notes, err := gs.store.ListAll()
	if err != nil {
		logMsg := "store ListAll failed: " + err.Error()
		log.Error(logMsg, "method: handleList")
		responseError(rw, CODE_SYSTEM_ERROR, logMsg)
		return
	}

	responseSuccess(rw, notes)
}
